package day28.homework;

/**
 * 生产者-消费者问题实现
 * 
 * 问题描述：
 * 1. 有多个生产者(Productor)每间隔1秒将生产的一个产品交给店员(Clerk)
 * 2. 有多个消费者(Customer)每间隔1秒可以从店员处购买一个产品
 * 3. 一个店员最多能管理20个产品
 * 4. 如果店员管理的产品数量超过20个，店员会叫生产者停止生产
 * 5. 如果店员管理的产品数量低于20个，再通知生产者继续生产
 * 6. 如果店员中没有产品了，店员会告诉消费者等一下，当有产品时，通知消费者购买产品
 */

// 产品类 - 代表一个具体的产品，具有唯一标识ID
class Product {
    // 产品的唯一标识符
    private int id;

    /**
     * 构造方法，用于创建一个具有指定ID的产品实例
     * @param id 产品的唯一标识符
     */
    public Product(int id) {
        this.id = id;
    }

    /**
     * 获取产品的ID
     * @return 产品的唯一标识符
     */
    public int getId() {
        return id;
    }

    /**
     * 重写toString方法，用于返回产品的字符串表示形式
     * @return 产品的字符串描述
     */
    @Override
    public String toString() {
        return "Product[" + id + "]";
    }
}

// 店员类 - 负责管理产品库存，是生产者和消费者之间的协调者
class Clerk {
    // 最大库存量常量，表示店员最多能管理的产品数量
    private static final int MAX_PRODUCTS = 20;
    // 当前库存量变量，记录当前店员手中的产品数量
    private int productCount = 0;

    /**
     * 生产产品方法 - 由生产者调用，用于向店员添加产品
     * 使用synchronized关键字确保线程安全，同一时间只有一个线程可以执行此方法
     * 这是解决线程安全问题的关键：保证对共享资源productCount的访问是互斥的
     */
    public synchronized void addProduct() {
        // 如果库存已满(大于等于最大库存量)，生产者需要等待
        // 使用while循环而不是if判断是为了防止虚假唤醒问题
        // 虚假唤醒：即使没有其他线程调用notify/notifyAll，线程也可能意外唤醒
        // 因此需要在唤醒后重新检查条件，确保条件确实满足
        while (productCount >= MAX_PRODUCTS) {
            System.out.println("库存已满(" + productCount + ")，生产者等待...");
            try {
                // 调用wait()方法使当前线程(生产者线程)进入等待状态，并释放对象锁
                // 等待其他线程(消费者线程)调用notify()/notifyAll()方法唤醒
                // wait()方法会自动释放当前对象的锁，允许其他线程进入同步方法
                wait(); // 等待消费者消费后唤醒
            } catch (InterruptedException e) {
                // 如果等待过程中线程被中断，打印异常信息
                e.printStackTrace();
            }
        }

        // 增加产品 - 当库存未满时，生产者可以添加产品到库存中
        productCount++;
        System.out.println(Thread.currentThread().getName() + "生产了一个产品，当前库存：" + productCount);

        // 唤醒所有等待的线程(包括生产者和消费者线程)
        // 消费者线程会被唤醒并检查是否可以购买产品
        // 使用notifyAll()而非notify()是为了避免死锁情况，确保所有等待线程都有机会竞争锁
        notifyAll();
    }

    /**
     * 消费产品方法 - 由消费者调用，用于从店员处购买产品
     * 使用synchronized关键字确保线程安全，同一时间只有一个线程可以执行此方法
     * synchronized关键字保证了方法级别的线程同步，防止多个线程同时修改productCount
     */
    public synchronized void sellProduct() {
        // 如果库存为空(产品数量小于等于0)，消费者需要等待
        // 使用while循环而不是if判断是为了防止虚假唤醒问题
        // 即使线程被唤醒，也需要重新检查条件是否满足，因为可能在等待期间条件已改变
        while (productCount <= 0) {
            System.out.println("库存为空(" + productCount + ")，消费者等待...");
            try {
                // 调用wait()方法使当前线程(消费者线程)进入等待状态，并释放对象锁
                // 等待其他线程(生产者线程)调用notify()/notifyAll()方法唤醒
                // 调用wait()方法会使当前线程释放Clerk对象的锁，允许其他线程执行同步方法
                wait(); // 等待生产者生产后唤醒
            } catch (InterruptedException e) {
                // 如果等待过程中线程被中断，打印异常信息
                e.printStackTrace();
            }
        }

        // 减少产品 - 当库存不为空时，消费者可以购买产品
        productCount--;
        System.out.println(Thread.currentThread().getName() + "购买了一个产品，当前库存：" + productCount);

        // 唤醒所有等待的线程(包括生产者和消费者线程)
        // 生产者线程会被唤醒并检查是否可以继续生产产品
        // notifyAll()确保所有在该对象上等待的线程都被唤醒，避免线程饥饿问题
        notifyAll();
    }
}

// 生产者类 - 实现Runnable接口，代表产品的生产者
class Producer implements Runnable {
    // 店员对象引用，生产者需要通过店员来存放产品
    private Clerk clerk;
    // 生产者名称，用于标识不同的生产者线程
    private String name;

    /**
     * 构造方法，用于创建生产者实例
     * @param clerk 店员对象引用
     * @param name 生产者名称
     */
    public Producer(Clerk clerk, String name) {
        this.clerk = clerk;
        this.name = name;
    }

    /**
     * 实现Runnable接口的run方法，定义线程执行的逻辑
     * run方法是线程启动后自动执行的方法体
     */
    @Override
    public void run() {
        // 设置当前线程的名称，便于区分不同的生产者线程
        // 通过设置线程名称可以在日志输出中清楚地识别是哪个线程在执行操作
        Thread.currentThread().setName(name);
        System.out.println(name + "开始生产产品...");
        
        try {
            // 无限循环，持续生产产品
            // 这模拟了实际生产环境中生产者持续工作的场景
            while (true) {
                // 调用店员的addProduct方法生产并添加产品到库存
                // 这里体现了生产者-消费者模式中的生产行为
                clerk.addProduct();
                
                // 模拟生产时间间隔，让线程休眠1秒钟
                // 这样可以观察到生产者和消费者交替工作的效果
                // 在实际应用中，生产可能需要时间来完成，这里用sleep模拟这个过程
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            // 如果线程在休眠过程中被中断，打印中断信息
            // InterruptedException通常在线程被其他线程中断时抛出
            System.out.println(name + "生产中断");
        }
    }
}

// 消费者类 - 实现Runnable接口，代表产品的消费者
class Consumer implements Runnable {
    // 店员对象引用，消费者需要通过店员来购买产品
    private Clerk clerk;
    // 消费者名称，用于标识不同的消费者线程
    private String name;

    /**
     * 构造方法，用于创建消费者实例
     * @param clerk 店员对象引用
     * @param name 消费者名称
     */
    public Consumer(Clerk clerk, String name) {
        this.clerk = clerk;
        this.name = name;
    }

    /**
     * 实现Runnable接口的run方法，定义线程执行的逻辑
     * run方法是线程启动后自动执行的方法体
     */
    @Override
    public void run() {
        // 设置当前线程的名称，便于区分不同的消费者线程
        // Thread.currentThread().setName(name)动态设置线程名称，便于调试和日志记录
        Thread.currentThread().setName(name);
        System.out.println(name + "开始购买产品...");
        
        try {
            // 无限循环，持续购买产品
            // 这模拟了实际消费环境中消费者持续消费的场景
            while (true) {
                // 调用店员的sellProduct方法从库存中购买产品
                // 这里体现了生产者-消费者模式中的消费行为
                clerk.sellProduct();
                
                // 模拟购买时间间隔，让线程休眠1秒钟
                // 这样可以观察到生产者和消费者交替工作的效果
                // 在实际应用中，消费也可能需要时间来完成，这里用sleep模拟这个过程
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            // 如果线程在休眠过程中被中断，打印中断信息
            // 当主线程调用interrupt()方法时，会抛出InterruptedException
            System.out.println(name + "购买中断");
        }
    }
}

/**
 * 主类 - 程序入口点，用于创建和启动生产者、消费者线程
 * 这个类演示了多线程环境下生产者-消费者模式的完整工作流程
 */
public class hw10 {
    /**
     * 程序主方法
     * @param args 命令行参数
     * main方法是Java程序的入口点，JVM会首先执行这个方法
     */
    public static void main(String[] args) {
        // 创建店员对象，作为生产者和消费者之间的中介
        // Clerk对象是共享资源，所有生产者和消费者线程都会访问同一个Clerk实例
        Clerk clerk = new Clerk();
        
        // 创建多个生产者线程
        // 生产者1线程，负责生产产品并交给店员
        // Thread构造函数接收Runnable接口实现类实例，将执行逻辑与线程管理分离
        Thread producer1 = new Thread(new Producer(clerk, "生产者1"));
        // 生产者2线程，负责生产产品并交给店员
        Thread producer2 = new Thread(new Producer(clerk, "生产者2"));
        
        // 创建多个消费者线程
        // 消费者1线程，负责从店员处购买产品
        // 每个Thread实例代表一个独立的执行线程
        Thread consumer1 = new Thread(new Consumer(clerk, "消费者1"));
        // 消费者2线程，负责从店员处购买产品
        Thread consumer2 = new Thread(new Consumer(clerk, "消费者2"));
        // 消费者3线程，负责从店员处购买产品
        Thread consumer3 = new Thread(new Consumer(clerk, "消费者3"));
        
        // 启动所有线程，使生产者和消费者开始工作
        // start()方法会创建新线程并调用对应Runnable的run()方法
        // 调用start()后，线程进入就绪状态，等待JVM调度执行
        producer1.start();  // 启动生产者1线程
        producer2.start();  // 启动生产者2线程
        consumer1.start();  // 启动消费者1线程
        consumer2.start();  // 启动消费者2线程
        consumer3.start();  // 启动消费者3线程
        
        // 运行一段时间后结束程序
        try {
            // 主线程休眠30秒，让生产者和消费者线程有足够时间运行
            // TimeUnit.MILLISECONDS.sleep(30000)提供了更清晰的时间单位表达
            Thread.sleep(30000); // 运行30秒
        } catch (InterruptedException e) {
            // 如果主线程在休眠过程中被中断，打印异常信息
            e.printStackTrace();
        }
        
        // 中断所有线程，优雅地结束程序
        // interrupt()方法会设置线程的中断状态，导致sleep()方法抛出InterruptedException
        producer1.interrupt();   // 中断生产者1线程
        producer2.interrupt();   // 中断生产者2线程
        consumer1.interrupt();   // 中断消费者1线程
        consumer2.interrupt();   // 中断消费者2线程
        consumer3.interrupt();   // 中断消费者3线程
        
        // 打印程序结束信息
        System.out.println("程序结束");
    }
}